package com.sq.shortlink.project.service.impl;

import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.date.Week;
import cn.hutool.core.text.StrBuilder;
import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.http.HttpUtil;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONObject;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.sq.shortlink.project.common.convension.exception.ClientException;
import com.sq.shortlink.project.common.convension.exception.ServiceException;
import com.sq.shortlink.project.common.enums.VailDataTypeEnum;
import com.sq.shortlink.project.dao.entity.*;
import com.sq.shortlink.project.dao.mapper.*;
import com.sq.shortlink.project.dto.req.ShortLinkCreateReqDTO;
import com.sq.shortlink.project.dto.req.ShortLinkPageReqDTO;
import com.sq.shortlink.project.dto.req.ShortLinkUpdateReqDTO;
import com.sq.shortlink.project.dto.resp.ShortLinkCreateRespDTO;
import com.sq.shortlink.project.dto.resp.ShortLinkGroupQueryCountRespDTO;
import com.sq.shortlink.project.dto.resp.ShortLinkPageRespDTO;
import com.sq.shortlink.project.service.ShortLinkGotoService;
import com.sq.shortlink.project.service.ShortLinkService;
import com.sq.shortlink.project.toolkit.ClientIPExtractor;
import com.sq.shortlink.project.toolkit.HashUtil;
import com.sq.shortlink.project.toolkit.LinkUtil;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.redisson.api.RBloomFilter;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.io.IOException;
import java.net.HttpURLConnection;
import java.net.URL;
import java.time.LocalDateTime;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;

import static com.sq.shortlink.project.common.constant.RedisKeyConstant.*;
import static com.sq.shortlink.project.common.constant.ShortLinkConstant.AMAP_REMOTE_URL;

/**
 * 短链接接口实现层
 * @author quan
 */
@Service
@Slf4j
@RequiredArgsConstructor
public class ShortLinkServiceImpl extends ServiceImpl<ShortLinkMapper, ShortLinkDO> implements ShortLinkService {

    private final RBloomFilter<String> shortLinkCreatePenetrationBloomFilter;
    private final ShortLinkGotoService shortLinkGotoService;
    private final LinkAccessStatsMapper linkAccessStatsMapper;
    private final LinkLocaleStatsMapper linkLocaleStatsMapper;
    private final LinkOsStatsMapper linkOsStatsMapper;
    private final LinkBrowserStatsMapper linkBrowserStatsMapper;
    private final LinkAccessLogMapper linkAccessLogMapper;
    private final LinkDeviceStatsMapper linkDeviceStatsMapper;
    private final LinkNetWorkStatsMapper linkNetWorkStatsMapper;
    private final LinkStatsTodayMapper linkStatsTodayMapper;
    private final StringRedisTemplate stringRedisTemplate;
    private final RedissonClient redissonClient;
    @Value("${short-link.stats.locale.amap-key}")
    private String statsLocaleAmapKey;
    @Override
    @Transactional(rollbackFor = Exception.class)
    public ShortLinkCreateRespDTO createShortLink(ShortLinkCreateReqDTO requestParam) throws Exception {
        String shortLinkSuffix = generateSuffix(requestParam);
        String fullShortUrl = StrBuilder.create(requestParam.getDomain())
                                        .append("/")
                                        .append(shortLinkSuffix).toString();
        ShortLinkDO shortLinkDO = BeanUtil.toBean(requestParam, ShortLinkDO.class);
        shortLinkDO.setFullShortUrl(fullShortUrl);
        shortLinkDO.setShortUri(shortLinkSuffix);
        shortLinkDO.setFavicon(getFavicon(requestParam.getOriginUrl()));
        shortLinkDO.setEnableStatus(0);
        shortLinkDO.setTotalPv(0);
        shortLinkDO.setTotalUv(0);
        shortLinkDO.setTotalUIp(0);
        try{
            baseMapper.insert(shortLinkDO);
        }catch (DuplicateKeyException ex){
            LambdaQueryWrapper<ShortLinkDO> queryWrapper = Wrappers.lambdaQuery(ShortLinkDO.class)
                    .eq(ShortLinkDO::getFullShortUrl, fullShortUrl);
            ShortLinkDO hasShortLinkDTO = baseMapper.selectOne(queryWrapper);
            if (hasShortLinkDTO != null) {
                log.warn("短链接：{} 重复入库",fullShortUrl);
                throw new ServiceException("短链接重复生成");
            }
        }
        shortLinkCreatePenetrationBloomFilter.add(fullShortUrl);
        ShortLinkGotoDO linkGotoDO = ShortLinkGotoDO.builder()
                .fullShortUrl(fullShortUrl)
                .gid(requestParam.getGid())
                .build();
        shortLinkGotoService.save(linkGotoDO);
        stringRedisTemplate.opsForValue().set(String.format(GOTO_SHORT_LINK_KEY,fullShortUrl),
                requestParam.getOriginUrl(),
                LinkUtil.getLinkValidTime(requestParam.getValidDate()),TimeUnit.MILLISECONDS);
        return ShortLinkCreateRespDTO.builder()
                .gid(requestParam.getGid())
                .fullShortUrl("http://" + shortLinkDO.getFullShortUrl())
                .originUrl(requestParam.getOriginUrl())
                .build();
    }

    @Override
    public IPage<ShortLinkPageRespDTO> pageShortLink(ShortLinkPageReqDTO requestParam) {
        LambdaQueryWrapper<ShortLinkDO> queryWrapper = Wrappers.lambdaQuery(ShortLinkDO.class)
                .eq(ShortLinkDO::getGid, requestParam.getGid())
                .eq(ShortLinkDO::getDelFlag, 0)
                .eq(ShortLinkDO::getEnableStatus, 0);
        IPage<ShortLinkDO> result = baseMapper.selectPage(requestParam, queryWrapper);
        return result.convert(each ->
                {
                    ShortLinkPageRespDTO linkPageRespDTO = BeanUtil.toBean(each, ShortLinkPageRespDTO.class);
                    linkPageRespDTO.setFullShortUrl("http://" + linkPageRespDTO.getFullShortUrl());
                    return linkPageRespDTO;
                }
        );
    }

    @Override
    public List<ShortLinkGroupQueryCountRespDTO> listGroupShortLinkCount(List<String> requestParam) {
        QueryWrapper<ShortLinkDO> queryWrapper = Wrappers.query(new ShortLinkDO())
                .select("gid as gid,count(*) as shortLinkCount")
                .eq("enable_status", 0)
                .eq("del_flag", 0)
                .in("gid", requestParam)
                .groupBy("gid");
        List<Map<String, Object>> maps = baseMapper.selectMaps(queryWrapper);
        return BeanUtil.copyToList(maps,ShortLinkGroupQueryCountRespDTO.class);
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public void updateShortLink(ShortLinkUpdateReqDTO requestParam) throws Exception {
        LambdaQueryWrapper<ShortLinkDO> queryWrapper = Wrappers.lambdaQuery(ShortLinkDO.class)
                .eq(ShortLinkDO::getFullShortUrl, requestParam.getFullShortUrl())
                .eq(ShortLinkDO::getGid, requestParam.getOldGid())
                .eq(ShortLinkDO::getEnableStatus, 0)
                .eq(ShortLinkDO::getDelFlag, 0);
        ShortLinkDO shortLinkDO = baseMapper.selectOne(queryWrapper);
        if (shortLinkDO == null) {
            throw new ClientException("用户记录不存在");
        }
        ShortLinkDO newShortLink = ShortLinkDO.builder()
                .clickNum(requestParam.getClickNum())
                .favicon(requestParam.getFavicon())
                .describe(requestParam.getDescribe())
                .originUrl(requestParam.getOriginUrl())
                .enableStatus(requestParam.getEnableStatus())
                .gid(requestParam.getNewGid())
                .validDateType(requestParam.getValidDateType())
                .validDate(requestParam.getValidDate())
                .createdType(shortLinkDO.getCreatedType())
                .domain(shortLinkDO.getDomain())
                .shortUri(shortLinkDO.getShortUri())
                .fullShortUrl(shortLinkDO.getFullShortUrl())
                .favicon(getFavicon(requestParam.getOriginUrl()))
                .build();
        if(Objects.equals(requestParam.getOldGid(),requestParam.getNewGid())){
            LambdaUpdateWrapper<ShortLinkDO> updateWrapper = Wrappers.lambdaUpdate(ShortLinkDO.class)
                    .eq(ShortLinkDO::getFullShortUrl, requestParam.getFullShortUrl())
                    .eq(ShortLinkDO::getGid, requestParam.getOldGid())
                    .eq(ShortLinkDO::getEnableStatus, 0)
                    .eq(ShortLinkDO::getDelFlag, 0)
                    .set(Objects.equals(requestParam.getValidDateType(), VailDataTypeEnum.PERMANENT.getType()),ShortLinkDO::getValidDate,null);
            baseMapper.update(newShortLink,updateWrapper);
        }else{
            LambdaUpdateWrapper<ShortLinkDO> updateWrapper = Wrappers.lambdaUpdate(ShortLinkDO.class)
                    .eq(ShortLinkDO::getFullShortUrl, requestParam.getFullShortUrl())
                    .eq(ShortLinkDO::getGid, shortLinkDO.getGid())
                    .eq(ShortLinkDO::getEnableStatus, 0)
                    .eq(ShortLinkDO::getDelFlag, 0);
            baseMapper.delete(updateWrapper);
            newShortLink.setEnableStatus(0);
            newShortLink.setValidDate(Objects.equals(requestParam.getValidDateType(),VailDataTypeEnum.PERMANENT.getType())?
                    null:requestParam.getValidDate());
            baseMapper.insert(newShortLink);
        }
    }



    @Override
    public void restoreUrl(String shortUri, HttpServletRequest request, HttpServletResponse response) throws IOException {
        //根据短链接uri查询到原始链接
        //1. 先查询到gid
        String fullShortUrl = StrBuilder.create(request.getServerName()).append("/").append(shortUri).toString();
        String originalUrl = stringRedisTemplate.opsForValue().get(String.format(GOTO_SHORT_LINK_KEY, fullShortUrl));
        if(StrUtil.isNotBlank(originalUrl)){
            shortLinkStats(fullShortUrl,null,request,response);
            response.sendRedirect(originalUrl);
            return;
        }
        boolean contains = shortLinkCreatePenetrationBloomFilter.contains(fullShortUrl);
        if (!contains) {
            //布隆过滤器不存在则直接返回
            response.sendRedirect("/page/notfound");
            return;
        }
        String isNullShortLink = stringRedisTemplate.opsForValue().get(String.format(GOTO_SHORT_LINK_ISNULL_KEY, fullShortUrl));
        if (StrUtil.isNotBlank(isNullShortLink)) {
            //如果之前缓存了空的也直接返回
            response.sendRedirect("/page/notfound");
            return;
        }
        RLock lock = redissonClient.getLock(String.format(LOCK_GOTO_SHORT_LINK_KEY, fullShortUrl));
        lock.lock();
        try {
            originalUrl = stringRedisTemplate.opsForValue().get(String.format(GOTO_SHORT_LINK_KEY, fullShortUrl));
            if(StrUtil.isNotBlank(originalUrl)){
                shortLinkStats(fullShortUrl,null,request,response);
                response.sendRedirect(originalUrl);
                return;
            }
            LambdaQueryWrapper<ShortLinkGotoDO> queryWrapper = Wrappers.lambdaQuery(ShortLinkGotoDO.class)
                    .eq(ShortLinkGotoDO::getFullShortUrl, fullShortUrl);
            ShortLinkGotoDO linkGotoDO = shortLinkGotoService.getOne(queryWrapper);
            if (linkGotoDO == null) {
                //缓存空对象
                stringRedisTemplate.opsForValue().set(String.format(GOTO_SHORT_LINK_ISNULL_KEY, fullShortUrl),"-",30L, TimeUnit.SECONDS);
                response.sendRedirect("/page/notfound");
                return ;
            }
            LambdaQueryWrapper<ShortLinkDO> queryWrapper1 = Wrappers.lambdaQuery(ShortLinkDO.class)
                    .eq(ShortLinkDO::getGid, linkGotoDO.getGid())
                    .eq(ShortLinkDO::getFullShortUrl, fullShortUrl)
                    .eq(ShortLinkDO::getEnableStatus, 0)
                    .eq(ShortLinkDO::getDelFlag, 0);
            ShortLinkDO shortLinkDO = baseMapper.selectOne(queryWrapper1);
            if(shortLinkDO == null ||(shortLinkDO.getValidDate() != null  && shortLinkDO.getValidDate().isBefore(LocalDateTime.now()))){
                //缓存空对象
                stringRedisTemplate.opsForValue().set(String.format(GOTO_SHORT_LINK_ISNULL_KEY, fullShortUrl),"-",30L, TimeUnit.SECONDS);
                response.sendRedirect("/page/notfound");
                return ;
            }
            stringRedisTemplate.opsForValue().set(String.format(GOTO_SHORT_LINK_KEY,fullShortUrl),
                    shortLinkDO.getOriginUrl(),
                    LinkUtil.getLinkValidTime(shortLinkDO.getValidDate()),TimeUnit.MILLISECONDS);
            shortLinkStats(fullShortUrl,shortLinkDO.getGid(),request,response);
            response.sendRedirect(shortLinkDO.getOriginUrl());
        } finally {
            lock.unlock();
        }
    }

    private void shortLinkStats(String fullShortUrl,String gid,HttpServletRequest request, HttpServletResponse response){
        AtomicBoolean uvFirstFlag = new AtomicBoolean();
        Cookie[] cookies = request.getCookies();
        try {
            AtomicReference<String> uv = new AtomicReference<>();
        Runnable addResponseCookieTask = () -> {
            uv.set(UUID.randomUUID().toString());
            Cookie uvCookie = new Cookie("uv",uv.get());
            uvCookie.setMaxAge(60 * 60 * 24 *30);
            uvCookie.setPath(StrUtil.sub(fullShortUrl,fullShortUrl.indexOf("/"),fullShortUrl.length()));
            response.addCookie(uvCookie);
            uvFirstFlag.set(Boolean.TRUE);
            stringRedisTemplate.opsForSet().add("short-link:stats:uv:" + fullShortUrl,uv.get());
        };
        if(ArrayUtil.isNotEmpty(cookies)){
            Arrays.stream(cookies)
                    .filter(each -> Objects.equals(each.getName(),"uv"))
                    .findFirst()
                    .map(Cookie::getValue)
                    .ifPresentOrElse(each -> {
                        uv.set(each);
                        Long added = stringRedisTemplate.opsForSet().add("short-link:stats:uv:" + fullShortUrl, each);
                        uvFirstFlag.set(added !=null && added > 0);
                    },addResponseCookieTask);
        }else{
            addResponseCookieTask.run();
        }

        if(gid == null){
            LambdaQueryWrapper<ShortLinkDO> queryWrapper = Wrappers.lambdaQuery(ShortLinkDO.class)
                    .eq(ShortLinkDO::getFullShortUrl, fullShortUrl);
            ShortLinkDO shortLinkDO = baseMapper.selectOne(queryWrapper);
            gid = shortLinkDO.getGid();
        }
            String clientIP = ClientIPExtractor.getClientIP(request);
            Long added = stringRedisTemplate.opsForSet().add("short-link:stats:uip:" + fullShortUrl, clientIP);
            boolean uipFirstFlag = added!=null && added > 0;
            int hour = DateUtil.hour(new Date(), true);
            Week week = DateUtil.dayOfWeekEnum(new Date());
            int weekValue = week.getIso8601Value();
            LinkAccessStatsDO linkAccessStatsDO = LinkAccessStatsDO.builder()
                    .pv(1)
                    .uv(uvFirstFlag.get()?1:0)
                    .uip(uipFirstFlag?1:0)
                    .date(new Date())
                    .fullShortUrl(fullShortUrl)
                    .gid(gid)
                    .hour(hour)
                    .weekday(weekValue)
                    .build();
            linkAccessStatsMapper.shortLinkStats(linkAccessStatsDO);
            Map<String,Object> localeParamMap = new HashMap<>();
            localeParamMap.put("key",statsLocaleAmapKey);
            localeParamMap.put("ip",clientIP);
            String localeResultStr = HttpUtil.get(AMAP_REMOTE_URL, localeParamMap);
            JSONObject localeResultObj = JSON.parseObject(localeResultStr);
            String infocode = localeResultObj.getString("infocode");
            if(StrUtil.isNotBlank(infocode) && "10000".equals(infocode)){
                String province = localeResultObj.getString("province");
                boolean unknownFlag = StrUtil.equals(province,"[]");
                province = unknownFlag?"未知":province;
                String city = unknownFlag?"未知":localeResultObj.getString("city");
                LinkLocaleStatsDO linkLocaleStatsDO = LinkLocaleStatsDO.builder()
                        .province(province)
                        .city(city)
                        .adcode(unknownFlag?"未知":localeResultObj.getString("adcode"))
                        .cnt(1)
                        .country("中国")
                        .date(new Date())
                        .fullShortUrl(fullShortUrl)
                        .gid(gid)
                        .build();
                linkLocaleStatsMapper.shortLinkLocaleStats(linkLocaleStatsDO);
                String os = LinkUtil.getOperatingSystem(request);
                LinkOsStatsDO linkOsStatsDO = LinkOsStatsDO.builder()
                        .cnt(1)
                        .date(new Date())
                        .fullShortUrl(fullShortUrl)
                        .gid(gid)
                        .os(os)
                        .build();
                linkOsStatsMapper.shortLinkOsStats(linkOsStatsDO);
                String browser = LinkUtil.getBrowser(request);
                LinkBrowserStatsDO linkBrowserStatsDO = LinkBrowserStatsDO.builder()
                        .cnt(1)
                        .date(new Date())
                        .fullShortUrl(fullShortUrl)
                        .gid(gid)
                        .browser(browser)
                        .build();
                linkBrowserStatsMapper.shortLinkOsStats(linkBrowserStatsDO);


                String device = LinkUtil.getDevice(request);
                LinkDeviceStatsDO linkDeviceStatsDO = LinkDeviceStatsDO.builder()
                        .fullShortUrl(fullShortUrl)
                        .gid(gid)
                        .date(new Date())
                        .device(device)
                        .cnt(1)
                        .build();
                linkDeviceStatsMapper.shortLinkDeviceStats(linkDeviceStatsDO);
                String network = LinkUtil.getNetwork(request);
                LinkNetWorkStatsDO linkNetWorkStatsDO = LinkNetWorkStatsDO.builder()
                        .fullShortUrl(fullShortUrl)
                        .gid(gid)
                        .date(new Date())
                        .network(network)
                        .cnt(1)
                        .build();
                linkNetWorkStatsMapper.shortLinkNetWorkStats(linkNetWorkStatsDO);

                LinkAccessLogsDO linkAccessLogsDO = LinkAccessLogsDO.builder()
                        .ip(clientIP)
                        .user(uv.get())
                        .browser(browser)
                        .os(os)
                        .fullShortUrl(fullShortUrl)
                        .gid(gid)
                        .network(network)
                        .device(device)
                        .locale(StrUtil.join("-","中国",province,city))
                        .build();
                linkAccessLogMapper.insert(linkAccessLogsDO);

                baseMapper.incrementStats(fullShortUrl,gid,1,uvFirstFlag.get()?1:0,uipFirstFlag?1:0);

                LinkStatsTodayDO linkStatsTodayDO = LinkStatsTodayDO.builder()
                        .fullShortUrl(fullShortUrl)
                        .gid(gid)
                        .date(new Date())
                        .todayPv(1)
                        .todayUv(uvFirstFlag.get()?1:0)
                        .todayUIp(uipFirstFlag?1:0)
                        .build();
                linkStatsTodayMapper.shortLinkStatsToday(linkStatsTodayDO);
            }
        } catch (Exception e) {
            log.warn("短链接访问统计量异常,{}",e);
        }
    }

    private String generateSuffix(ShortLinkCreateReqDTO requestParam) {
        int customGenerateCount = 0;
        String shortUri;
        while(true){
            if(customGenerateCount>10){
                throw new ServiceException("短链接重复生成");
            }
            String originUrl = requestParam.getOriginUrl();
            originUrl += System.currentTimeMillis();
            shortUri = HashUtil.hashToBase62(originUrl);
            if(!shortLinkCreatePenetrationBloomFilter.contains(requestParam.getDomain() + "/" + shortUri)){
                break;
            }
        }
        return shortUri;
    }

    /**
     * 获取网站图标
     * @param url 网站链接
     * @return 图标
     */
    private String getFavicon(String url) throws Exception{
        URL targetUrl = new URL(url);
        HttpURLConnection connection = (HttpURLConnection) targetUrl.openConnection();
        connection.setRequestMethod("GET");
        connection.connect();
        int responseCode = connection.getResponseCode();
        if(responseCode == HttpURLConnection.HTTP_OK){
            Document document = Jsoup.connect(url).get();
            Element faviconLink = document.select("link[rel~=(?i)^(shortcut )?icon]").first();
            return faviconLink!=null ? faviconLink.attr("abs:href") : null;
        }
        return null;
    }
}
